/**
 * Go.3D
 * @author Rob Simmons
 * @copyright 2012
 */

var Go3D = {
	/**
	 * @var string the most recently clicked intersection ID.
	 * This click could be the selection of a group, an invalid move or valid move.
	 * Intersection IDs are in the format <frontToBack>-<topToBottom>-<leftToRight>,
	 * where each of those values is an integer ranging from 1 to this.goban.gridSize
	 */
	lastIntersectId : undefined,

	/**
	 * @var float the number of points white is awarded for not getting to go first or get a handicap
	 */
	komi : 6.5,

	/**
	 * @var boolean whether the previous turn was a pass
	 */
	justPassed : false,

	/**
	 * @var boolean whether the game is over
	 */
	isGameOver : false,

	/**
	 * @var whether a stone group's territory (if any) should be highlighted when that stone group is selected
	 */
	shouldHighlightTerritory : true,

	/**
	 * Start the game
	 */
	init : function() {
		this.players.init();
		this.goban.init();
		this.hud.init();
	},


	/**
	 * Manage players
	 */
	players : {
		white : undefined,
		black : undefined,

		/**
		 * Create players
		 */
		init : function() {
			this.white = new Go3D.Player('white');
			this.black = new Go3D.Player('black');
		},

		/**
		 * Get the color of the player whose turn it is
		 *
		 * @return string 'white' or 'black'
		 */
		getCurrentColor : function() {
			return this.white.isMyTurn() ? 'white' : 'black';
		},

		/**
		 * Get the player whose turn it is
		 *
		 * @return Player
		 */
		getCurrentPlayer : function() {
			return this.white.isMyTurn() ? this.white : this.black;
		},

		/**
		 * Get the color of the player whose turn it isn't
		 *
		 * @return string 'white' or 'black'
		 */
		getNonCurrentColor : function() {
			return this.white.isMyTurn() ? 'black' : 'white';
		},

		/**
		 * Get the player whose turn it isn't
		 *
		 * @return Player
		 */
		getNonCurrentPlayer : function() {
			return this.white.isMyTurn() ? this.black : this.white;
		},

		updateTerritory : function() {
			var frontToBack,
				topToBottom,
				leftToRight,
				accountedForIntersectIds = [],
				intersectId,
				territory,
				vacantIntersectIds,
				color,
				blackStoneIntersectIds,
				whiteStoneIntersectIds,
				isStonePresent;

			jQuery('.territory').removeClass('territory');

			this.white.resetTerritory();
			this.black.resetTerritory();

			for (frontToBack = 1; frontToBack <= Go3D.goban.gridSize; frontToBack++) {
				for (topToBottom = 1; topToBottom <= Go3D.goban.gridSize; topToBottom++) {
					for (leftToRight = 1; leftToRight <= Go3D.goban.gridSize; leftToRight++) {

						intersectId = frontToBack + '-' + topToBottom + '-' + leftToRight;
						if (accountedForIntersectIds.indexOf(intersectId) === -1) {
							// if no stone present
							isStonePresent = !!Go3D.stoneGroups.getStone(intersectId);
							if (!isStonePresent) {

								territory = new Go3D.Territory(intersectId);
								vacantIntersectIds = territory.getIntersectIds();
								blackStoneIntersectIds = territory.getStoneIntersectIds('black');
								whiteStoneIntersectIds = territory.getStoneIntersectIds('white');
								accountedForIntersectIds = accountedForIntersectIds.concat(vacantIntersectIds, blackStoneIntersectIds, whiteStoneIntersectIds);

								if (territory.isValid()) {
									color = territory.getColor();
									this[color].addTerritory(territory);
								}
							}

						}
					}
				}
			}
		}, // end of updateTerritory

		/**
		 * Swap whose turn it is
		 */
		doneWithTurn : function() {
			this.white.toggleIsMyTurn();
			this.black.toggleIsMyTurn();
		}
	}, // end of players


	/**
	 * Track a player's attributes:
	 * their color, is it their turn, their last move,
	 * and how many of their opponent's stones they've captured.
	 *
	 * @param color string 'black' or 'white'
	 */
	Player : function(color) {
		if (color !== 'white' && color !== 'black') {
			alert('Player() was called with an invalid color: "' + color + '"')
		}

		/**
		 * @var string the intersect ID of this players most recent valid move
		 */
		var lastIntersectId;

		/**
		 * @var string the intersect ID of this players most recently killed group (of his color)
		 */
		var lastKilledIntersectId;

		/**
		 * @var boolean whether it is this players turn
		 */
		var isMyTurn = color == 'black';

		/**
		 * @var int the number stones this player has captured
		 */
		var capturedCount = 0;

		/**
		 * @var array the territories this player holds
		 */
		var territories = [];


		return {
			getColor : function() {
				return color;
			},

			getLastKilledIntersectId : function() {
				return lastKilledIntersectId;
			},

			setLastKilledIntersectId : function(intersectId) {
				lastKilledIntersectId = intersectId;
			},

			getLastIntersectId : function() {
				return lastIntersectId;
			},

			updateLastIntersectId : function() {
				lastIntersectId = Go3D.lastIntersectId;
			},

			isMyTurn : function() {
				return isMyTurn;
			},

			toggleIsMyTurn : function() {
				isMyTurn = !isMyTurn;
			},

			incrCapturedCount : function(count) {
				capturedCount += count;
			},

			getCapturedCount : function() {
				return capturedCount;
			},

			getTerritoryCount : function() {
				var count = 0;
				for (var i = 0; i < territories.length; i++) {
					count += territories[i].getSize();
				}
				return count;
			},

			getTerritories : function() {
				return territories;
			},

			addTerritory : function(territory) {
				territories.push(territory);
			},

			removeTerritory : function(territory) {
				territories.splice(territories.indexOf(territory), 1);
			},

			resetTerritory : function() {
				territories = [];
			}
		}
	}, // end of Player


	/**
	 * The board/cube.
	 * This deals with drawing the board and listening to clicks.
	 */
	goban : {
		/**
		 * @var int the number of intersections in one dimension
		 * e.g. a value of 5 represents a 5x5x5 goban.
		 * I believe only values of 5 and 7 are surmountable by the human mind.
		 */
		gridSize : 5,

		/**
		 * Draw and initialize the goban
		 */
		init : function() {
			var frontToBack,
				topToBottom,
				leftToRight,
				intersectId,
				intersectHtml,
				goban2dId,
				goban2dHtml,
				goban3dId,
				goban3dHtml,
				cellWidth,
				cellBorderWidth,
				goban2dWidth,
				goban2dMargin,
				goban3dWidth;



			goban3dId = 'frontToBack';
			goban3dHtml = '<div id="' + goban3dId + '" class="goban3d"></div>';
			jQuery('body').append(goban3dHtml);

			for (frontToBack = 1; frontToBack <= this.gridSize; frontToBack++) {
				goban2dId = goban3dId + '-' + frontToBack;
				goban2dHtml = '<div id="' + goban2dId + '" class="goban2d"></div>';
				jQuery('#' + goban3dId).append(goban2dHtml);
				for (topToBottom = 1; topToBottom <= this.gridSize; topToBottom++) {
					for (leftToRight = 1; leftToRight <= this.gridSize; leftToRight++) {
						intersectId = frontToBack + '-' + topToBottom + '-' + leftToRight;
						intersectHtml = '<div class="' + intersectId + ' cell" title="' + intersectId + '"></div>';
						jQuery('#' + goban2dId).append(intersectHtml);

					}
				}
			}


			goban3dId = 'topToBottom';
			goban3dHtml = '<div id="' + goban3dId + '" class="goban3d"></div>';
			jQuery('body').append(goban3dHtml);

			for (topToBottom = 1; topToBottom <= this.gridSize; topToBottom++) {
				goban2dId = goban3dId + '-' + topToBottom;
				goban2dHtml = '<div id="' + goban2dId + '" class="goban2d"></div>';
				jQuery('#' + goban3dId).append(goban2dHtml);

				for (frontToBack = this.gridSize; frontToBack >= 1; frontToBack--) {
					for (leftToRight = 1; leftToRight <= this.gridSize; leftToRight++) {
						intersectId = frontToBack + '-' + topToBottom + '-' + leftToRight;
						intersectHtml = '<div class="' + intersectId + ' cell" title="' + intersectId + '"></div>';
						jQuery('#' + goban2dId).append(intersectHtml);
					}
				}
			}


			goban3dId = 'leftToRight';
			goban3dHtml = '<div id="' + goban3dId + '" class="goban3d"></div>';
			jQuery('body').append(goban3dHtml);

			for (leftToRight = 1; leftToRight <= this.gridSize; leftToRight++) {
				goban2dId = goban3dId + '-' + leftToRight;
				goban2dHtml = '<div id="' + goban2dId + '" class="goban2d"></div>';
				jQuery('#' + goban3dId).append(goban2dHtml);

				for (topToBottom = 1; topToBottom <= this.gridSize; topToBottom++) {
					for (frontToBack = this.gridSize; frontToBack >= 1; frontToBack--) {
						intersectId = frontToBack + '-' + topToBottom + '-' + leftToRight;
						intersectHtml = '<div class="' + intersectId + ' cell" title="' + intersectId + '"></div>';
						jQuery('#' + goban2dId).append(intersectHtml);
					}
				}
			}


			/**
			 * Dynamic goban styling
			 */
			// todo make the width adjust to fill the screen w/o wrapping
			cellWidth = jQuery('.cell').css('width');
			cellWidth = cellWidth.replace('px', '');
			// todo get cellBorderWidth from css
			cellBorderWidth = 1;
			cellBorderWidth = 2*cellBorderWidth;
			goban2dWidth = (cellWidth*1 + cellBorderWidth) * this.gridSize;
			jQuery('.goban2d').css({
				'width': goban2dWidth,
				'height': goban2dWidth
			});

			// todo get goban2dMargin from css
//			goban2dMargin = 10;//jQuery('.goban2d').css('margin');
//			//goban2dMargin = goban2dMargin.replace('px', '');
//			goban3dWidth = (goban2dWidth + 2*goban2dMargin) * this.gridSize;
//			jQuery('.goban3d').css({
//				'width' : goban3dWidth
//				//'height' : goban2dWidth
//			});


			// click listener
			jQuery('.cell').click(this.placeStone);
		},

		/**
		 * Deal with clicking on the goban.
		 * If clicking an existing stone group, select or unselect it.
		 * Otherwise attempt to place a stone and update statistics.
		 */
		placeStone : function() {
			var isStonePresent,
				validMove;


			Go3D.lastIntersectId = Go3D.getIntersectIdFromElement(this);

			/**
			 * If clicking on an existing stone, select/highlight that stone-group
			 */
			isStonePresent = this.children.length == 1;
			if (isStonePresent) {
				Go3D.stoneGroups.selectGroup();
				return;
			}

			if (Go3D.isGameOver) {
				Go3D.stoneGroups.unselectGroups();
				jQuery('.click-selected').removeClass('click-selected');
				jQuery('.territory').removeClass('territory');
				return;
			}

			// validate and add stone to board / process move
			validMove = Go3D.stoneGroups.validateAndUpdate();
			if (validMove) {
				Go3D.players.getCurrentPlayer().updateLastIntersectId();

				// remove old most-recent-moves highlights
				jQuery('.most-recent-1').removeClass('most-recent-1');
				jQuery('.most-recent-2').removeClass('most-recent-2');
				jQuery('.click-selected').removeClass('click-selected');

				// add new most-recent-moves highlights
				jQuery('.' + Go3D.players.getCurrentPlayer().getLastIntersectId()).addClass('most-recent-1');
				jQuery('.' + Go3D.players.getNonCurrentPlayer().getLastIntersectId()).addClass('most-recent-2');


				Go3D.players.doneWithTurn();
				Go3D.hud.update();
				Go3D.justPassed = false;
				Go3D.players.updateTerritory();
			}
		}
	}, // end of goban


	/**
	 * Manage stone groups
	 */
	stoneGroups : {

		/**
		 * @var array of StoneGroup s
		 */
		groups : [],

		/**
		 * Create/manage collection of stoneGroups
		 * even a single stone counts as a stoneGroup
		 * @param intersectId
		 */
		validateAndUpdate : function() {
			var stone,
				group,
				firstGroup,
				friendlyNeighbors,
				i,
				isOpposingColor,
				isKillMove,
				isSuicide,
				isValidMove,
				isKo,
				indicesOfGroupsToDelete,
				setLastKilled,
				hasStones,
				isSamePlaceAsLastKilled,
				isOnlyOneGroupToKill,
				isOnlyOneStoneToKill;

			this.unselectGroups();

			// create newest stone
			stone = new Go3D.Stone();

			// if newest stone is directly touching any existing stone group(s), add it to the group(s)
			friendlyNeighbors = stone.getFriendlyNeighborIntersectIds();
			if (friendlyNeighbors.length > 0) {
				// add to first group
				firstGroup = this.getGroup(friendlyNeighbors[0]);
				firstGroup.addStone(stone);

				// merge multiple groups if this new stone bridges the gap between them
				if (friendlyNeighbors.length > 1) {
					for (i = 1; i < friendlyNeighbors.length; i++) {
						group = this.getGroup(friendlyNeighbors[i]);
						if (group === firstGroup) {
							continue;
						}

						firstGroup.merge(group);
						this.deleteGroup(group);
					}
				}
			}
			// otherwise not touching any other stones, so make a new single-stone stone group
			else {
				firstGroup = new Go3D.StoneGroup(stone);
				this.groups.push(firstGroup);
			}


			// collect dead stone groups
			isKillMove = false;
			indicesOfGroupsToDelete = [];
			for (i = 0; i < this.groups.length; i++) {
				isOpposingColor = this.groups[i].getColor() !== Go3D.players.getCurrentColor();
				if (isOpposingColor && this.groups[i].isDead()) {
					indicesOfGroupsToDelete.push(i);
					isKillMove = true;
				}
			}


			// determine if this is an illegal move, a suicide or ko
			isSuicide = !isKillMove && firstGroup.isDead();
			
			isSamePlaceAsLastKilled = stone.getIntersectId() == Go3D.players.getCurrentPlayer().getLastKilledIntersectId();
			isOnlyOneGroupToKill = indicesOfGroupsToDelete.length == 1;
			isOnlyOneStoneToKill = isOnlyOneGroupToKill && this.groups[indicesOfGroupsToDelete[0]].getStones().length == 1;
			isKo = isKillMove && isSamePlaceAsLastKilled && isOnlyOneStoneToKill;

			if (isSuicide) {
				alert('suicide!');
			} else if (isKo) {
				alert('ko!');
			}

			// undo move if it wasn't a legal move
			isValidMove = true;
			if (isSuicide || isKo) {
				firstGroup.removeStone(stone);
				if (firstGroup.getStones().length == 0) {
					firstGroup.removeHtml();
					this.deleteGroup(firstGroup);
				}
				isValidMove = false;
			}

			// remove any dead stone groups
			if (isValidMove && isKillMove) {
				for (i = 0; i < indicesOfGroupsToDelete.length; i++) {
					group = this.groups[indicesOfGroupsToDelete[i]];
					/**
					 * If a single-stone kill, keep track of this for calculation of ko in subsequent turn
					 */
					if (group.getStones().length == 1) {
						Go3D.players.getNonCurrentPlayer().setLastKilledIntersectId(group.getStones()[0].getIntersectId());
						setLastKilled = true;
					}
					group.removeHtml();
					this.deleteGroup(group);
				}
			}

			if (!setLastKilled) {
				Go3D.players.getNonCurrentPlayer().setLastKilledIntersectId('');
			}

			return isValidMove;
		},

		/**
		 * Retrieve a stone with a specified intersect ID
		 *
		 * @param intersectId string
		 * @return Stone|false
		 */
		getStone : function(intersectId) {
			var stone;
			intersectId = intersectId || Go3D.lastIntersectId;

			for (var i = 0; i < this.groups.length; i++) {
				stone = this.groups[i].getStone(intersectId);
				if (stone) {
					return stone;
				}
			}

			return false;
		},

		/**
		 * Remove a stone group from the stone group collection
		 *
		 * @param group StoneGroup
		 */
		deleteGroup : function(group) {
			this.groups.splice(this.groups.indexOf(group), 1);
		},

		/**
		 * Remove highlight from the group
		 */
		unselectGroups : function() {
			jQuery('.selected').removeClass('selected');
		},

		/**
		 * Highlight/unhighlight the clicked stone group
		 */
		selectGroup : function() {
			var stones,
				territories,
				group = this.getGroup(Go3D.lastIntersectId),
				i,
				j,
				stone,
				stoneWasClickSelected;


			stone = Go3D.stoneGroups.getStone();
			stoneWasClickSelected = stone.isClickSelected();

			jQuery('.territory').removeClass('territory');
			jQuery('.click-selected').removeClass('click-selected');


			if (group) {
				if (stoneWasClickSelected) {
					group.unselect();
				} else {
					group.select();
					stone.clickSelect();

					// grab corresponding territories and highlight them
					if (Go3D.shouldHighlightTerritory) {
						stones = group.getStones();
						territories = Go3D.players.white.getTerritories().concat(Go3D.players.black.getTerritories());
						// for every stone
						for (i = 0; i < stones.length; i++) {
							// for every territory
							for (j = 0; j < territories.length; j++) {
								if (territories[j].getAllStoneIntersectIds().indexOf(stones[i].getIntersectId()) !== -1) {
									territories[j].highlight();
								}
							}
						}
					}
				}
			}
		},

		/**
		 * Retrieve the stoneGroup that intersectId belongs to
		 *
		 * @param intersectId string
		 * @return StoneGroup|false
		 */
		getGroup : function(intersectId) {
			intersectId = intersectId || Go3D.lastIntersectId;
			for (var i = 0; i < this.groups.length; i++) {
				if (this.groups[i].contains(intersectId)) {
					return this.groups[i];
				}
			}
			return false;
		}

	}, // end of stoneGroups


	/**
	 * Make a new stone group.
	 * One or more stones of the same color which are chained together
	 * by the straight lines of the board (no diags).
	 *
	 * @param stone Stone the initial stone in the group
	 * @return StoneGroup
	 */
	StoneGroup : function(stone) {
		var stones = [stone],
			color = Go3D.players.getCurrentColor();


		return {
			/**
			 * Add stone to group
			 *
			 * @param stone Stone
			 */
			addStone : function(stone) {
				stones.push(stone);
			},

			/**
			 * Remove stone from goban and stone group
			 * @param stone Stone
			 */
			removeStone : function(stone) {
				for (var i = 0; i < stones.length; i++) {
					if (stones[i] === stone) {
						stone.removeHtml();
						stones.splice(i, 1);
						return;
					}
				}
			},

			/**
			 * Whether the group is selected/highlighted
			 *
			 * @return boolean
			 */
			isSelected : function() {
				return stones[0].isSelected();
			},

			/**
			 * Select/highlight group
			 */
			select : function() {
				// remove old selection
				this.unselect();
				// add new selection
				for (var i = 0; i < stones.length; i++) {
					stones[i].select();
				}
			},

			/**
			 * Unhighlight
			 */
			unselect : function() {
				jQuery('.selected').removeClass('selected');
			},

			/**
			 * @return string 'white' or 'black'
			 */
			getColor : function() {
				return color;
			},

			/**
			 * Whether this stone group has zero liberties
			 *
			 * @return boolean
			 */
			isDead : function() {
				var totalLibertiesCount = 0;
				for (var i = 0; i < stones.length; i++) {
					totalLibertiesCount += stones[i].getLibertiesCount();
				}
				return totalLibertiesCount == 0;
			},

			/**
			 * Remove the stone groups HTML
			 */
			removeHtml : function() {
				Go3D.players.getCurrentPlayer().incrCapturedCount(stones.length);
				for (var i = 0; i < stones.length; i++) {
					stones[i].removeHtml();
				}
			},

			/**
			 * Get stones array
			 *
			 * @return array of Stone s
			 */
			getStones : function() {
				return stones;
			},

			/**
			 * Get stone with a specified intersect ID from this group
			 *
			 * @param intersectId string
			 * @return Stone|false
			 */
			getStone : function(intersectId) {
				for (var i = 0; i < stones.length; i++) {
					if (stones[i].getIntersectId() == intersectId) {
						return stones[i];
					}
				}
				return false;
			},

			/**
			 * Whether this group contains a stone with a specified intersect ID
			 *
			 * @param intersectId string
			 * @return boolean
			 */
			contains : function(intersectId) {
				return !!this.getStone(intersectId);
			},

			/**
			 * Add the stones from another group into this group
			 *
			 * @param group StoneGroup
			 */
			merge : function(group) {
				var incomingStones = group.getStones();
				for (var i = 0; i < incomingStones.length; i++) {
					this.addStone(incomingStones[i]);
				}
			}
		}
	}, // end of StoneGroup


	/**
	 * Make a new stone
	 *
	 * @return Stone
	 */
	Stone : function() {
		var color = Go3D.players.getCurrentColor(),
			intersectId = Go3D.lastIntersectId;

		// add the stone's HTML
		var stoneHtml = '<div class="stone ' + color + '"></div>';
		jQuery('.' + intersectId).append(stoneHtml);


		return {
			/**
			 * @return string insersectId
			 */
			getIntersectId : function() {
				return intersectId;
			},

			/**
			 * @return string 'black' or 'white'
			 */
			getColor : function() {
				return color;
			},

			/**
			 * Remove the stone's HTML
			 */
			removeHtml : function() {
				jQuery('.' + intersectId).html('');
			},

			/**
			 * Highlight this stone, because it's part of a selected group
			 */
			select : function() {
				jQuery('.' + intersectId + ' .stone').addClass('selected');
			},

			/**
			 * Whether this stone has been highlighted
			 *
			 * @return boolean
			 */
			isSelected : function() {
				return jQuery('.' + intersectId + ' .stone').hasClass('selected');
			},

			/**
			 * Give this stone a special highlight indicating it was clicked
			 */
			clickSelect : function() {
				jQuery('.' + intersectId + ' .stone').addClass('click-selected');
			},

			/**
			 * Whether this stone has been click selected
			 */
			isClickSelected : function() {
				return jQuery('.' + intersectId + ' .stone').hasClass('click-selected');
			},

			/**
			 * Calculate the number of liberties for this stone
			 *
			 * @return int
			 */
			getLibertiesCount : function() {
				var neighborIntersectIds = this.getNeighborIntersectIds();
				var totalCount = neighborIntersectIds.length;
				var meanCount = this.getMeanNeighborIntersectIds(neighborIntersectIds).length;
				var friendlyCount = this.getFriendlyNeighborIntersectIds(neighborIntersectIds).length;

				return totalCount - meanCount - friendlyCount;
			},

			/**
			 * Get intersect IDs of neighboring intersects (whether empty or occupied)
			 *
			 * @return array of string intersectIds
			 */
			getNeighborIntersectIds : function() {
				var frontToBack = Go3D.getFrontToBack(intersectId),
					topToBottom = Go3D.getTopToBottom(intersectId),
					leftToRight = Go3D.getLeftToRight(intersectId),
					neighbors = [];

				// front
				if (frontToBack > 1) {
					neighbors.push((frontToBack*1-1) + '-' + topToBottom + '-' + leftToRight);
				}

				// back
				if (frontToBack < Go3D.goban.gridSize) {
					neighbors.push((frontToBack*1+1) + '-' + topToBottom + '-' + leftToRight);
				}

				// top
				if (topToBottom > 1) {
					neighbors.push(frontToBack + '-' + (topToBottom*1-1) + '-' + leftToRight);
				}

				// bottom
				if (topToBottom < Go3D.goban.gridSize) {
					neighbors.push(frontToBack + '-' + (topToBottom*1+1) + '-' + leftToRight);
				}

				// left
				if (leftToRight > 1) {
					neighbors.push(frontToBack + '-' + topToBottom + '-' + (leftToRight*1-1));
				}

				// right
				if (leftToRight < Go3D.goban.gridSize) {
					neighbors.push(frontToBack + '-' + topToBottom + '-' + (leftToRight*1+1));
				}


				return neighbors;
			},

			/**
			 * Get intersect IDs of neighboring stones of a given color
			 *
			 * @param color string 'black' or 'white'
			 * @param neighborIntersectIds array of intersect Ids
			 * @return array of intersect IDs
			 */
			getNeighborStoneIntersectIds : function(color, neighborIntersectIds) {
				var neighborElement,
					connected = [],
					i;

				neighborIntersectIds = neighborIntersectIds || this.getNeighborIntersectIds();

				for (i = 0; i < neighborIntersectIds.length; i++) {
					neighborElement = jQuery('.' + neighborIntersectIds[i])[0];
					if (neighborElement.children.length > 0) {
						if (jQuery(neighborElement.children[0]).hasClass(color)) {
							connected.push(neighborIntersectIds[i]);
						}
					}
				}

				return connected;
			},

			getFriendlyNeighborIntersectIds : function(neighborIntersectIds) {
				return this.getNeighborStoneIntersectIds(color, neighborIntersectIds);
			},

			getMeanNeighborIntersectIds : function(neighborIntersectIds) {
				return this.getNeighborStoneIntersectIds(color == 'white' ? 'black' : 'white', neighborIntersectIds);
			}
		};
	}, // end of Stone


	/**
	 * Territory, a contiguous group of empty intersections completely surrounded by a single player/color.
	 * isValid is set to false if this potential territory touches stones of both white and black, and is thus,
	 * not actually a territory.
	 */
	Territory : function(intersectId) {
		var intersectIds = [intersectId];
		var intersections = [jQuery('.' + intersectId)];
		var color;
		var isValid = true;
		var stoneIntersectIds = {
			black : [],
			white : []
		};

		/**
		 * A territory attempts to build itself, stemming from the supplied
		 * intersectId
		 */
		(function build(intersectId) {
			var frontToBack = Go3D.getFrontToBack(intersectId);
			var topToBottom = Go3D.getTopToBottom(intersectId);
			var leftToRight = Go3D.getLeftToRight(intersectId);
			var testIntersectIds = [];
			var newEmptyIntersectIds = [];

			/**
			 * Record intersect ID on Territory object, under the corresponding intersection type--empty, white, or black.
			 * @param intersectId
			 */
			var processIntersectId = function(intersectId) {
				var stone = Go3D.stoneGroups.getStone(intersectId);
				if (stone) {
					color = stone.getColor();
					stoneIntersectIds[color].push(intersectId);

					// this is not actually a territory if stones of both colors touch it
					if (stoneIntersectIds.black.length > 0 && stoneIntersectIds.white.length > 0) {
						isValid = false;
						return false;
					}
				} else {
					if (intersectIds.indexOf(intersectId) === -1) {
						intersectIds.push(intersectId);
						intersections.push(jQuery('.' + intersectId));
						newEmptyIntersectIds.push(intersectId);
					}
				}

				return true;
			};

			if (!isValid) {
				return;
			}


			// front
			if (frontToBack > 1) {
				testIntersectIds.push((frontToBack*1 - 1) + '-' + topToBottom + '-' + leftToRight);
			}

			// back
			if (frontToBack < Go3D.goban.gridSize) {
				testIntersectIds.push((frontToBack*1 + 1) + '-' + topToBottom + '-' + leftToRight);
			}

			// top
			if (topToBottom > 1) {
				testIntersectIds.push(frontToBack + '-' + (topToBottom*1 - 1) + '-' + leftToRight);
			}

			// bottom
			if (topToBottom < Go3D.goban.gridSize) {
				testIntersectIds.push(frontToBack + '-' + (topToBottom*1 + 1) + '-' + leftToRight);
			}

			// left
			if (leftToRight > 1) {
				testIntersectIds.push(frontToBack + '-' + topToBottom + '-' + (leftToRight*1 - 1));
			}

			// right
			if (leftToRight < Go3D.goban.gridSize) {
				testIntersectIds.push(frontToBack + '-' + topToBottom + '-' + (leftToRight*1 + 1));
			}

			// record / analyze results of surrounding intersections
			for (var i = 0; i < testIntersectIds.length; i++) {
				if (!processIntersectId(testIntersectIds[i])) {
					return;
				}
			}

			// recurse on vacant spots that haven't been visited yet
			for (i = 0; i < newEmptyIntersectIds.length; i++) {
				build(newEmptyIntersectIds[i]);
			}

		})(intersectId);



		return {
			getIntersectIds : function() {
				return intersectIds;
			},

			getIntersections : function() {
				return intersections;
			},

			getStoneIntersectIds : function(color) {
				return stoneIntersectIds[color];
			},

			getAllStoneIntersectIds : function() {
				return this.getStoneIntersectIds('white').concat(this.getStoneIntersectIds('black'));
			},

			getColor : function() {
				return isValid ? color : false;
			},

			getSize : function() {
				return isValid ? this.getIntersections().length : false;
			},

			highlight : function() {
				if (!isValid) {
					return;
				}

				for (var i = 0; i < intersections.length; i++) {
					intersections[i].addClass('territory');
				}
			},

			unHighlight : function() {
				if (!isValid) {
					return;
				}
				jQuery('.territory').removeClass('territory');
			},

			addIntersections : function(newIntersectIds) {
				if (!isValid) {
					return;
				}
				for (var i = 0; i < newIntersectIds.length; i++) {
					intersections.push(jQuery('.' + newIntersectIds[i]));
					intersectIds.push(newIntersectIds[i]);
				}

				return intersectIds.length;
			},

			removeIntersections : function(removeIntersectIds) {
				if (!isValid) {
					return;
				}
				var index;
				for (var i = 0; i < removeIntersectIds.length; i++) {
					index = intersectIds.indexOf(removeIntersectIds[i]);
					if (index === -1) {
						continue;
					}
					intersectIds.splice(index, 1);
					intersections.splice(index, 1);
				}
			},

			contains : function(intersectId) {
				return intersectIds.indexOf(intersectId) !== -1;
			},

			isValid : function() {
				return isValid;
			}
		}
	}, // end of Territory


	/**
	 * Manage display of stats and pass and resign buttons
	 */
	hud : {
		hudElement : undefined,
		turnDisplayElement : undefined,
		blackPointsElement : undefined,
		whitePointsElement : undefined,
		passElement : undefined,
		resignElement : undefined,

		init : function() {
			var hudHtml,
				displayTurnHtml,
				blackPointsHtml,
				whitePointsHtml,
				passButtonHtml,
				resignButtonHtml;

			hudHtml = '<div id="hud"></div>';
			jQuery('body').append(hudHtml);
			this.hudElement = jQuery('#hud');

			displayTurnHtml = '<p id="displayTurn"><span></span>\'s turn</p>';
			this.hudElement.append(displayTurnHtml);
			this.turnDisplayElement = jQuery('#displayTurn span');

			blackPointsHtml = '<p id="blackPoints">Black captured <span></span> stone(s)</p>';
			whitePointsHtml = '<p id="whitePoints">White captured <span></span> stone(s)</p>';
			this.hudElement.append(blackPointsHtml);
			this.hudElement.append(whitePointsHtml);
			this.blackPointsElement = jQuery('#blackPoints span');
			this.whitePointsElement = jQuery('#whitePoints span');

			passButtonHtml = '<button type="button" id="pass">Pass!</button>';
			this.hudElement.append(passButtonHtml);
			this.passElement = jQuery('#pass');

			this.passElement.click(this.processPass);

			resignButtonHtml = '<button type="button" id="resign">Resign!</button>';
			this.hudElement.append(resignButtonHtml);
			this.resignElement = jQuery('#resign');
			this.resignElement.click(this.resign);



			this.update();
		},

		update : function() {
			this.updateTurnDisplay();
			this.updateScore();
		},

		updateTurnDisplay : function() {
			this.turnDisplayElement.html(Go3D.players.getCurrentColor());
		},

		updateScore : function() {
			var whiteScore = Go3D.players.white.getCapturedCount();
			var blackScore = Go3D.players.black.getCapturedCount();
			this.whitePointsElement.html(whiteScore);
			this.blackPointsElement.html(blackScore);
		},

		processPass : function() {
			if (Go3D.isGameOver) {
				return;
			}
			if (Go3D.justPassed) {
				Go3D.gameOver();
			} else {
				Go3D.players.doneWithTurn();
				Go3D.hud.update();
				Go3D.justPassed = true;
			}
		},

		resign : function() {
			if (Go3D.isGameOver || !confirm("Throw in the towel?")) {
				return;
			}
			Go3D.isGameOver = true;
			Go3D.hud.setWinner(Go3D.players.getNonCurrentColor());
		},

		setWinner : function(winner, winnersPoints, losersPoints) {
			var winnerHtml = '<p>' +
				winner + ' wins, ' +
				((winnersPoints || losersPoints)
					? winnersPoints + ' to ' + losersPoints
					: Go3D.players.getCurrentColor() + ' resigned') +
				'</p>';
			
			this.hudElement.append(winnerHtml);
		}
	}, //  end of hud


	gameOver : function() {
		this.isGameOver = true;
	
		var blackLingering = this.removeLingeringDead('black');
		var whiteLingering = this.removeLingeringDead('white');

		var whiteTerritory = this.players.white.getTerritoryCount();
		var blackTerritory = this.players.black.getTerritoryCount();

		var whiteCaptured = this.players.white.getCapturedCount();
		var blackCaptured = this.players.black.getCapturedCount();

		var whitePoints = whiteTerritory - blackCaptured + blackLingering + this.komi;
		var blackPoints = blackTerritory - whiteCaptured + whiteLingering;

		var winner = whitePoints > blackPoints ? 'white' : 'black';
		var winnersPoints = winner == 'white' ? whitePoints : blackPoints;
		var losersPoints = winner == 'white' ? blackPoints : whitePoints;

		this.hud.setWinner(winner, winnersPoints, losersPoints);
	},


	/**
	 * 
	 * @param color
	 */
	removeLingeringDead : function(color) {
		return 0; // the number of stones of this color that are dead on the board

		// todo agree upon and/or remove all lingering dead groups

	},


	/**
	 * Some utility methods for dealing with intersection IDs
	 */

	
	/**
	 * Retrieve the intersect ID from an HTML element's class-list.
	 *
	 * @param element HTMLElement, a cell of the goban
	 */
	getIntersectIdFromElement : function(element) {
		return element.className.match(/[\d]-[\d]-[\d]/)[0];
	},

	/**
	 * Get the part of the intersectId that corresponds to the front-to-back value.
	 *
	 * @param intersectId string
	 * @return string
	 */
	getFrontToBack : function(intersectId) {
		return intersectId.split('-')[0];
	},

	/**
	 * Get the part of the intersectId that corresponds to the top-to-bottom value.
	 *
	 * @param intersectId string
	 * @return string
	 */
	getTopToBottom : function(intersectId) {
		return intersectId.split('-')[1];
	},

	/**
	 * Get the part of the intersectId that corresponds to the left-to-right value.
	 *
	 * @param intersectId string
	 * @return string
	 */
	getLeftToRight : function(intersectId) {
		return intersectId.split('-')[2];
	}
};
